/*
 * Copyright 2019. Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.shared.physics;

import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import java.util.List;

/** A utility class for physics-related functions. */
public final class Util {
    private static final String TAG = Util.class.getSimpleName();
    private static final float EPSILON = 0.0001f;
    private static final int COLLINEAR = 0;
    private static final int CLOCKWISE = 1;
    private static final int COUNTERCLOCKWISE = 2;

    private Util() {
        // Don't allow instantiation of this class.
    }

    /**
     * Return whether the point is within the axis-aligned rectangle defined by p and q.
     *
     * @param p first bounding point.
     * @param q second bounding point.
     * @param point the point to check.
     * @return true if point is within the bounds defined by p and q, false otherwise.
     */
    public static boolean pointIsWithinBounds(Vector2D p, Vector2D q, Vector2D point) {
        return point.x >= Math.min(p.x, q.x)
                && point.x <= Math.max(p.x, q.x)
                && point.y >= Math.min(p.y, q.y)
                && point.y <= Math.max(p.y, q.y);
    }

    /**
     * Find the orientation of the ordered triplet of points. They are either clockwise,
     * counterclockwise, or collinear. Implementation based on: http://goo.gl/a44iML
     */
    private static int orientation(Vector2D p, Vector2D q, Vector2D r) {
        float value = (q.y - p.y) * (r.x - q.x) - (r.y - q.y) * (q.x - p.x);

        // Use this instead of Math.abs(value) here because it is faster.
        if (value < EPSILON && value > -EPSILON) {
            return COLLINEAR;
        }
        return value > 0 ? CLOCKWISE : COUNTERCLOCKWISE;
    }

    /**
     * Compute whether or not lines 1 and 2 intersect. Implementation based on:
     * http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
     *
     * @param p1 The starting point of line 1
     * @param q1 The ending point of line 1
     * @param p2 The starting point of line 2
     * @param q2 The ending point of line 2
     * @return true if 1 and 2 intersect, false otherwise.
     */
    public static boolean lineSegmentIntersectsLineSegment(
            Vector2D p1, Vector2D q1, Vector2D p2, Vector2D q2) {
        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        // General case
        if (o1 != o2 && o3 != o4) {
            return true;
        }

        // Special cases
        if (o1 == 0 && pointIsWithinBounds(p1, q1, p2)
                || o2 == 0 && pointIsWithinBounds(p1, q1, q2)
                || o3 == 0 && pointIsWithinBounds(p2, q2, p1)
                || o4 == 0 && pointIsWithinBounds(p2, q2, q1)) {
            return true;
        }
        return false;
    }

    /**
     * Return whether or not two rectangles intersect. This uses a basic form of the separating axis
     * theorem which should be faster than running a full polygon-to-polygon check.
     *
     * @return true if the rectangles intersect, false otherwise.
     */
    public static boolean rectIntersectsRect(
            float x1, float y1, float w1, float h1, float x2, float y2, float w2, float h2) {
        float halfWidth1 = w1 / 2;
        float halfWidth2 = w2 / 2;
        float halfHeight1 = h1 / 2;
        float halfHeight2 = h2 / 2;

        float horizontalThreshold = halfWidth1 + halfWidth2;
        float verticalThreshold = halfHeight1 + halfHeight2;

        float horizontalDistance = Math.abs(x1 + halfWidth1 - (x2 + halfWidth2));
        float verticalDistance = Math.abs(y1 + halfHeight1 - (y2 + halfHeight2));

        return horizontalDistance < horizontalThreshold && verticalDistance < verticalThreshold;
    }

    /**
     * Use the separating axis theorem to determine whether or not two convex polygons intersect.
     *
     * @return true if the polygons intersect, false otherwise.
     */
    public static boolean convexPolygonIntersectsConvexPolygon(Polygon p1, Polygon p2) {
        for (int i = 0; i < p1.normals.size(); i++) {
            Vector2D normal = p1.normals.get(i);
            float p1Min = getMinProjectionInDirection(normal, p1.vertices);
            float p1Max = getMaxProjectionInDirection(normal, p1.vertices);
            float p2Min = getMinProjectionInDirection(normal, p2.vertices);
            float p2Max = getMaxProjectionInDirection(normal, p2.vertices);
            if (p1Max < p2Min || p2Max < p1Min) {
                // If there is a separating axis, these polygons do not intersect.
                return false;
            }
        }
        for (int i = 0; i < p2.normals.size(); i++) {
            Vector2D normal = p2.normals.get(i);
            float p1Min = getMinProjectionInDirection(normal, p1.vertices);
            float p1Max = getMaxProjectionInDirection(normal, p1.vertices);
            float p2Min = getMinProjectionInDirection(normal, p2.vertices);
            float p2Max = getMaxProjectionInDirection(normal, p2.vertices);
            if (p1Max < p2Min || p2Max < p1Min) {
                // If there is a separating axis, these polygons do not intersect.
                return false;
            }
        }
        return true;
    }

    private static float getMaxProjectionInDirection(Vector2D direction, List<Vector2D> points) {
        float max = points.get(0).dot(direction);
        for (int i = 1; i < points.size(); i++) {
            max = Math.max(max, points.get(i).dot(direction));
        }
        return max;
    }

    private static float getMinProjectionInDirection(Vector2D direction, List<Vector2D> points) {
        float min = points.get(0).dot(direction);
        for (int i = 1; i < points.size(); i++) {
            min = Math.min(min, points.get(i).dot(direction));
        }
        return min;
    }

    public static Vector2D getMidpoint(Vector2D p, Vector2D q) {
        float deltaX = q.x - p.x;
        float deltaY = q.y - p.y;
        return Vector2D.get(p.x + deltaX / 2, p.y + deltaY / 2);
    }

    public static float clamp(float value, float lowerBound, float upperBound) {
        return Math.max(lowerBound, Math.min(value, upperBound));
    }

    public static int clamp(int value, int lowerBound, int upperBound) {
        return Math.max(lowerBound, Math.min(value, upperBound));
    }
}
